﻿// Contributed by: panfinsen.
// Converted from VB to C# using http://www.developerfusion.com/tools/convert/vb-to-csharp/
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;


namespace TelnetExpect
{
    /// <summary>
    /// Contributed by: panfinsen.
    /// A version of <see cref="T:TelnetExpect.Expector"/> which supports
    /// limited length regular expressions as the pattern passed to Expect.
    /// </summary>
    /// -
    /// <remarks>
    /// <para>&#x201C;Shamelessly reflected Expector into RegExpector, which
    /// supports limited length regular expressions as the pattern passed to
    /// Expect. Also added a ReadDuringExpect property to easily capture the
    /// characters received during an Expect call.&#x201D;
    /// </para>
    /// </remarks>
    /// -
    /// <example>
    /// <code>
    /// Dim Username as String = ...
    /// Dim Password as String = ...
    /// '
    /// xp.Expect("login:\s*$")
    /// Debug.WriteLine("login prompt received...")
    /// xp.SendLine(Username)
    /// xp.Expect("Password:\s*$")
    /// Debug.WriteLine("password prompt received...")
    /// xp.SendLine(Password)
    /// xp.Expect(String.Format("(?i){0}>\s*$", Username))
    /// Trace.TraceInformation("Logged on.")
    /// _LoggedOn = True
    /// </code>
    /// </example>
    public class RegExpector : IDisposable
    {
        private static readonly TimeSpan DefaultExpectTimeout;
        private char[] buffer;
        private TextReader reader;
        private int bufferLength;
        private TextWriter writer;

        private Nullable<DateTime> lastWaitStarted;
        public string ReadDuringExpect
        {
            get { return _ReadDuringExpect.ToString(); }
        }

        private StringBuilder _ReadDuringExpect;
        public TimeSpan ExpectTimeout { get; set; }

        static RegExpector()
        {
            RegExpector.DefaultExpectTimeout = new TimeSpan(0, 2, 0);
        }

        public RegExpector(Stream conn)
        {
            this.ExpectTimeout = RegExpector.DefaultExpectTimeout;
            this._ReadDuringExpect = new StringBuilder();
            this.buffer = new char[0x400];
            reader = new StreamReader(conn, Encoding.ASCII);
            writer = new StreamWriter(conn, Encoding.ASCII);
        }

        /// <summary>
        /// Write a line to the stream
        /// </summary>
        /// <param name="txt">The text of the line to write</param>
        public void SendLine(string txt)
        {
            this.writer.WriteLine(txt);
            this.writer.Flush();
        }

        /// <summary>
        /// Read the stream, looking for a pattern.
        /// </summary>
        /// <param name="pattern">The regular expression pattern to find. This pattern must match no more than 132 characters.</param>
        /// <exception cref="EndOfStreamException">Thrown when the timeout expires</exception>
        public void Expect(string pattern)
        {
            Trace.WriteLine("----------------");
            Trace.WriteLine(string.Format(CultureInfo.InvariantCulture,
                "Expect [[{0}]]", new object[] { TelnetBaseStream.UtilityShowControlChars(pattern) }));
            this.lastWaitStarted = new Nullable<DateTime>(DateTime.UtcNow);
            ManualResetEvent completed = null;
            try {
                this.RegisterTimeoutIfEnabled(ref (completed));
                this._ReadDuringExpect.Length = 0;
                this.WaitFor(pattern);
            } finally {
                // Set the wait object for next time
                if (completed != null)
                    completed.Set();
            }
        }

        private void WaitFor(string pattern)
        {
            Regex re = new Regex(pattern);
            Match m = null;
            while (true) {
                // str is what has been received so far, converted to a string
                string str = new string(this.buffer, 0, this.bufferLength);
                m = re.Match(str);
                Trace.WriteLine(string.Format(CultureInfo.InvariantCulture, "Expect {0} find match in [[{1}]]", new object[] {
				    ((m.Success) ? "did" : "didn't"),
				    TelnetBaseStream.UtilityShowControlChars(str)
			    }));
                if ((m.Success)) {
                    // Eliminate all characters in the buffer up to and including the string to find
                    int iFirstCharAfterFoundString = (m.Index + m.Length);
                    int iRemainingCharsInBuffer = (this.bufferLength - iFirstCharAfterFoundString);
                    // copy all characters after the found string to the beginning of the buffer
                    Array.Copy(this.buffer, iFirstCharAfterFoundString, this.buffer, 0, iRemainingCharsInBuffer);
                    // Update the buffer length
                    this.bufferLength = iRemainingCharsInBuffer;
                } else {
                    // Clear the buffer except for the last n characters, which might constitute a partial match due to not having
                    // received all characters.
                    // Since regular expressions can match a variable number of characters, pick an arbitrary number and save up to 
                    // that many characters from the end of the buffer. This is also the maximum number of characters which may be 
                    // matched by the regex pattern.
                    int iPossiblePartialMatchCount = 132;
                    iPossiblePartialMatchCount = Math.Min(this.bufferLength, iPossiblePartialMatchCount);
                    // copy the last iPossiblePartialMatchCount characters to the beginning of the buffer
                    Array.Copy(this.buffer, (this.bufferLength - iPossiblePartialMatchCount), this.buffer, 0, iPossiblePartialMatchCount);
                    // Update the buffer length
                    this.bufferLength = iPossiblePartialMatchCount;
                }

                string PossiblePartialMatchCharacters = new string(this.buffer, 0, this.bufferLength);
                Trace.WriteLine(string.Format(CultureInfo.InvariantCulture, "Keeping: [[{0}]]", new object[] { TelnetBaseStream.UtilityShowControlChars(PossiblePartialMatchCharacters) }));
                if ((m.Success))
                    return;

                int count = (this.buffer.Length - this.bufferLength);
                if ((count <= 0)) {
                    throw new InvalidOperationException("We're gonna need a bigger buffer!!!!!!!");
                }

                // Read a maximum of count characters from the stream and writes them to the end of the buffer
                int iCharsRead = 0;
                try {
                    iCharsRead = 0;
                    iCharsRead = this.reader.Read(this.buffer, this.bufferLength, count);
                } catch (IOException exIO) {
                }

                if ((iCharsRead == 0)) {
                    throw new EndOfStreamException("No match before end of input.");
                }

                // Note what was read
                _ReadDuringExpect.Append(new string(this.buffer, this.bufferLength, iCharsRead));

                // Update the buffer length
                this.bufferLength = (this.bufferLength + iCharsRead);
            }
        }

        private void ExpectTimeoutHandler(object state, bool timedOut)
        {
            if (timedOut) {
                Trace.WriteLine("Expect is closing connection due to timeout.");
                try {
                    this.writer.Close();
                } finally {
                    this.reader.Close();
                }
            }
        }

        private void RegisterTimeoutIfEnabled(ref ManualResetEvent completed)
        {
            if (((this.ExpectTimeout != TimeSpan.MinValue) && (this.ExpectTimeout != TimeSpan.Zero))) {
                completed = new ManualResetEvent(false);
                ThreadPool.RegisterWaitForSingleObject(completed, new WaitOrTimerCallback(this.ExpectTimeoutHandler), null, this.ExpectTimeout, true);
            }
        }

        #region "IDisposable Support"
        // To detect redundant calls
        private bool disposedValue;

        // IDisposable
        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposedValue) {
                if (disposing) {
                    // dispose managed state (managed objects).
                    try {
                        this.writer.Close();
                    } finally {
                        this.reader.Close();
                    }
                }
            }
            this.disposedValue = true;
        }

        // This code added by Visual Basic to correctly implement the disposable pattern.
        public void Dispose()
        {
            // Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion

    }
}